From 7fd5243c4602e25b2860d159bc50288081cbe50b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 5 Jul 2016 10:28:51 -0700 Subject: [PATCH] Implement a directory source This flavor of source is intended to behave like a local registry except that its contents are unpacked rather than zipped up in `.crate` form. Like with local registries the only way to use this currently is via the `.cargo/config`-based source replacement currently, and primarily only to replace crates.io or other registries at the moment. A directory source is simply a directory which has many `.crate` files unpacked inside of it. The directory is not recursively traversed for changes, but rather it is just required that all elements in the directory are themselves directories of packages. This format is more suitable for checking into source trees, and it still provides guarantees around preventing modification of the original source from the upstream copy. Each directory in the directory source is required to have a `.cargo-checksum.json` file indicating the checksum it *would* have had if the crate had come from the original source as well as all of the sha256 checksums of all the files in the repo. It is intended that directory sources are assembled from a separately shipped subcommand (e.g. `cargo vendor` or `cargo local-registry`), so these checksum files don't have to be managed manually. Modification of a directory source is not the intended purpose, and if a modification is detected then the user is nudged towards solutions like `[replace]` which are intended for overriding other sources and processing local modifications. --- src/cargo/core/source.rs | 36 +++ src/cargo/ops/cargo_rustc/fingerprint.rs | 9 + src/cargo/sources/config.rs | 9 + src/cargo/sources/directory.rs | 153 +++++++++++ src/cargo/sources/mod.rs | 10 +- src/cargo/sources/replaced.rs | 7 +- tests/cargotest/support/registry.rs | 6 +- tests/cfg.rs | 4 +- tests/directory.rs | 335 +++++++++++++++++++++++ tests/install.rs | 4 +- tests/local-registry.rs | 9 +- tests/publish.rs | 5 +- 12 files changed, 571 insertions(+), 16 deletions(-) create mode 100644 src/cargo/sources/directory.rs create mode 100644 tests/directory.rs diff --git a/src/cargo/core/source.rs b/src/cargo/core/source.rs index 93e223a29..ed684e954 100644 --- a/src/cargo/core/source.rs +++ b/src/cargo/core/source.rs @@ -14,6 +14,7 @@ use core::{Package, PackageId, Registry}; use ops; use sources::git; use sources::{PathSource, GitSource, RegistrySource, CRATES_IO}; +use sources::DirectorySource; use util::{human, Config, CargoResult, ToUrl}; /// A Source finds and downloads remote packages based on names and @@ -38,6 +39,17 @@ pub trait Source: Registry { /// The `pkg` argument is the package which this fingerprint should only be /// interested in for when this source may contain multiple packages. fn fingerprint(&self, pkg: &Package) -> CargoResult; + + /// If this source supports it, verifies the source of the package + /// specified. + /// + /// Note that the source may also have performed other checksum-based + /// verification during the `download` step, but this is intended to be run + /// just before a crate is compiled so it may perform more expensive checks + /// which may not be cacheable. + fn verify(&self, _pkg: &PackageId) -> CargoResult<()> { + Ok(()) + } } impl<'a, T: Source + ?Sized + 'a> Source for Box { @@ -52,6 +64,10 @@ impl<'a, T: Source + ?Sized + 'a> Source for Box { fn fingerprint(&self, pkg: &Package) -> CargoResult { (**self).fingerprint(pkg) } + + fn verify(&self, pkg: &PackageId) -> CargoResult<()> { + (**self).verify(pkg) + } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -64,6 +80,8 @@ enum Kind { Registry, /// represents a local filesystem-based registry LocalRegistry, + /// represents a directory-based registry + Directory, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -172,6 +190,9 @@ impl SourceId { SourceIdInner { kind: Kind::LocalRegistry, ref url, .. } => { format!("local-registry+{}", url) } + SourceIdInner { kind: Kind::Directory, ref url, .. } => { + format!("directory+{}", url) + } } } @@ -194,6 +215,11 @@ impl SourceId { Ok(SourceId::new(Kind::LocalRegistry, url)) } + pub fn for_directory(path: &Path) -> CargoResult { + let url = try!(path.to_url()); + Ok(SourceId::new(Kind::Directory, url)) + } + /// Returns the `SourceId` corresponding to the main repository. /// /// This is the main cargo registry by default, but it can be overridden in @@ -253,6 +279,13 @@ impl SourceId { }; Box::new(RegistrySource::local(self, &path, config)) } + Kind::Directory => { + let path = match self.inner.url.to_file_path() { + Ok(p) => p, + Err(()) => panic!("path sources cannot be remote"), + }; + Box::new(DirectorySource::new(&path, self, config)) + } } } @@ -342,6 +375,9 @@ impl fmt::Display for SourceId { SourceIdInner { kind: Kind::LocalRegistry, ref url, .. } => { write!(f, "registry {}", url) } + SourceIdInner { kind: Kind::Directory, ref url, .. } => { + write!(f, "dir {}", url) + } } } } diff --git a/src/cargo/ops/cargo_rustc/fingerprint.rs b/src/cargo/ops/cargo_rustc/fingerprint.rs index e8ad1880f..91a8b1abb 100644 --- a/src/cargo/ops/cargo_rustc/fingerprint.rs +++ b/src/cargo/ops/cargo_rustc/fingerprint.rs @@ -57,6 +57,15 @@ pub fn prepare_target<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, let compare = compare_old_fingerprint(&loc, &*fingerprint); log_compare(unit, &compare); + if compare.is_err() { + let source_id = unit.pkg.package_id().source_id(); + let sources = cx.packages.sources(); + let source = try!(sources.get(source_id).chain_error(|| { + internal("missing package source") + })); + try!(source.verify(unit.pkg.package_id())); + } + let root = cx.out_dir(unit); let mut missing_outputs = false; if unit.profile.doc { diff --git a/src/cargo/sources/config.rs b/src/cargo/sources/config.rs index 3be515645..e40b6a7d5 100644 --- a/src/cargo/sources/config.rs +++ b/src/cargo/sources/config.rs @@ -137,6 +137,15 @@ a lock file compatible with `{orig}` cannot be generated in this situation path.push(s); srcs.push(try!(SourceId::for_local_registry(&path))); } + if let Some(val) = table.get("directory") { + let (s, path) = try!(val.string(&format!("source.{}.directory", + name))); + let mut path = path.to_path_buf(); + path.pop(); + path.pop(); + path.push(s); + srcs.push(try!(SourceId::for_directory(&path))); + } let mut srcs = srcs.into_iter(); let src = try!(srcs.next().chain_error(|| { diff --git a/src/cargo/sources/directory.rs b/src/cargo/sources/directory.rs new file mode 100644 index 000000000..84a9501a0 --- /dev/null +++ b/src/cargo/sources/directory.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use rustc_serialize::hex::ToHex; +use rustc_serialize::json; + +use core::{Package, PackageId, Summary, SourceId, Source, Dependency, Registry}; +use sources::PathSource; +use util::{CargoResult, human, ChainError, Config, Sha256}; +use util::paths; + +pub struct DirectorySource<'cfg> { + id: SourceId, + root: PathBuf, + packages: HashMap, + config: &'cfg Config, +} + +#[derive(RustcDecodable)] +struct Checksum { + package: String, + files: HashMap, +} + +impl<'cfg> DirectorySource<'cfg> { + pub fn new(path: &Path, id: &SourceId, config: &'cfg Config) + -> DirectorySource<'cfg> { + DirectorySource { + id: id.clone(), + root: path.to_path_buf(), + config: config, + packages: HashMap::new(), + } + } +} + +impl<'cfg> Debug for DirectorySource<'cfg> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "DirectorySource {{ root: {:?} }}", self.root) + } +} + +impl<'cfg> Registry for DirectorySource<'cfg> { + fn query(&mut self, dep: &Dependency) -> CargoResult> { + let packages = self.packages.values().map(|p| &p.0); + let matches = packages.filter(|pkg| dep.matches(pkg.summary())); + let summaries = matches.map(|pkg| pkg.summary().clone()); + Ok(summaries.collect()) + } + + fn supports_checksums(&self) -> bool { + true + } +} + +impl<'cfg> Source for DirectorySource<'cfg> { + fn update(&mut self) -> CargoResult<()> { + self.packages.clear(); + let entries = try!(self.root.read_dir().chain_error(|| { + human(format!("failed to read root of directory source: {}", + self.root.display())) + })); + + for entry in entries { + let entry = try!(entry); + let path = entry.path(); + let mut src = PathSource::new(&path, + &self.id, + self.config); + try!(src.update()); + let pkg = try!(src.root_package()); + + let cksum_file = path.join(".cargo-checksum.json"); + let cksum = try!(paths::read(&path.join(cksum_file)).chain_error(|| { + human(format!("failed to load checksum `.cargo-checksum.json` \ + of {} v{}", + pkg.package_id().name(), + pkg.package_id().version())) + + })); + let cksum: Checksum = try!(json::decode(&cksum).chain_error(|| { + human(format!("failed to decode `.cargo-checksum.json` of \ + {} v{}", + pkg.package_id().name(), + pkg.package_id().version())) + })); + + let mut manifest = pkg.manifest().clone(); + let summary = manifest.summary().clone(); + manifest.set_summary(summary.set_checksum(cksum.package.clone())); + let pkg = Package::new(manifest, pkg.manifest_path()); + self.packages.insert(pkg.package_id().clone(), (pkg, cksum)); + } + + Ok(()) + } + + fn download(&mut self, id: &PackageId) -> CargoResult { + self.packages.get(id).map(|p| &p.0).cloned().chain_error(|| { + human(format!("failed to find package with id: {}", id)) + }) + } + + fn fingerprint(&self, pkg: &Package) -> CargoResult { + Ok(pkg.package_id().version().to_string()) + } + + fn verify(&self, id: &PackageId) -> CargoResult<()> { + let (pkg, cksum) = match self.packages.get(id) { + Some(&(ref pkg, ref cksum)) => (pkg, cksum), + None => bail!("failed to find entry for `{}` in directory source", + id), + }; + + let mut buf = [0; 16 * 1024]; + for (file, cksum) in cksum.files.iter() { + let mut h = Sha256::new(); + let file = pkg.root().join(file); + + try!((|| -> CargoResult<()> { + let mut f = try!(File::open(&file)); + loop { + match try!(f.read(&mut buf)) { + 0 => return Ok(()), + n => h.update(&buf[..n]), + } + } + }).chain_error(|| { + human(format!("failed to calculate checksum of: {}", + file.display())) + })); + + let actual = h.finish().to_hex(); + if &*actual != cksum { + bail!("\ + the listed checksum of `{}` has changed:\n\ + expected: {}\n\ + actual: {}\n\ + \n\ + directory sources are not intended to be edited, if \ + modifications are required then it is recommended \ + that [replace] is used with a forked copy of the \ + source\ + ", file.display(), cksum, actual); + } + } + + Ok(()) + } +} diff --git a/src/cargo/sources/mod.rs b/src/cargo/sources/mod.rs index 53c573aa3..ed784e95a 100644 --- a/src/cargo/sources/mod.rs +++ b/src/cargo/sources/mod.rs @@ -1,11 +1,13 @@ -pub use self::path::PathSource; +pub use self::config::SourceConfigMap; +pub use self::directory::DirectorySource; pub use self::git::GitSource; +pub use self::path::PathSource; pub use self::registry::{RegistrySource, CRATES_IO}; pub use self::replaced::ReplacedSource; -pub use self::config::SourceConfigMap; -pub mod path; +pub mod config; +pub mod directory; pub mod git; +pub mod path; pub mod registry; -pub mod config; pub mod replaced; diff --git a/src/cargo/sources/replaced.rs b/src/cargo/sources/replaced.rs index cd0ffd4b1..7fb95bdf6 100644 --- a/src/cargo/sources/replaced.rs +++ b/src/cargo/sources/replaced.rs @@ -50,6 +50,11 @@ impl<'cfg> Source for ReplacedSource<'cfg> { } fn fingerprint(&self, id: &Package) -> CargoResult { - self.inner.fingerprint(id) + self.inner.fingerprint(&id) + } + + fn verify(&self, id: &PackageId) -> CargoResult<()> { + let id = id.with_source_id(&self.replace_with); + self.inner.verify(&id) } } diff --git a/tests/cargotest/support/registry.rs b/tests/cargotest/support/registry.rs index da3e12bbf..12e857211 100644 --- a/tests/cargotest/support/registry.rs +++ b/tests/cargotest/support/registry.rs @@ -132,7 +132,7 @@ impl Package { self } - pub fn publish(&self) { + pub fn publish(&self) -> String { self.make_archive(); // Figure out what we're going to write into the index @@ -197,6 +197,8 @@ impl Package { "Another commit", &tree, &[&parent])); } + + return cksum } fn make_archive(&self) { @@ -255,7 +257,7 @@ impl Package { } } -fn cksum(s: &[u8]) -> String { +pub fn cksum(s: &[u8]) -> String { let mut sha = Sha256::new(); sha.update(s); sha.finish().to_hex() diff --git a/tests/cfg.rs b/tests/cfg.rs index adbdfc3e6..e8de0101b 100644 --- a/tests/cfg.rs +++ b/tests/cfg.rs @@ -221,8 +221,8 @@ fn works_through_the_registry() { [UPDATING] registry [..] [DOWNLOADING] [..] [DOWNLOADING] [..] -[COMPILING] foo v0.1.0 ([..]) -[COMPILING] bar v0.1.0 ([..]) +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.1.0 [COMPILING] a v0.0.1 ([..]) [FINISHED] debug [unoptimized + debuginfo] target(s) in [..] ")); diff --git a/tests/directory.rs b/tests/directory.rs new file mode 100644 index 000000000..aa6808769 --- /dev/null +++ b/tests/directory.rs @@ -0,0 +1,335 @@ +#[macro_use] +extern crate cargotest; +extern crate hamcrest; +extern crate rustc_serialize; + +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::prelude::*; +use std::str; + +use rustc_serialize::json; + +use cargotest::support::{project, execs, ProjectBuilder}; +use cargotest::support::paths; +use cargotest::support::registry::{Package, cksum}; +use hamcrest::assert_that; + +fn setup() { + let root = paths::root(); + t!(fs::create_dir(&root.join(".cargo"))); + t!(t!(File::create(root.join(".cargo/config"))).write_all(br#" + [source.crates-io] + registry = 'https://wut' + replace-with = 'my-awesome-local-registry' + + [source.my-awesome-local-registry] + directory = 'index' + "#)); +} + +struct VendorPackage { + p: Option, + cksum: Checksum, +} + +#[derive(RustcEncodable)] +struct Checksum { + package: String, + files: HashMap, +} + +impl VendorPackage { + fn new(name: &str) -> VendorPackage { + VendorPackage { + p: Some(project(&format!("index/{}", name))), + cksum: Checksum { + package: String::new(), + files: HashMap::new(), + }, + } + } + + fn file(&mut self, name: &str, contents: &str) -> &mut VendorPackage { + self.p = Some(self.p.take().unwrap().file(name, contents)); + self.cksum.files.insert(name.to_string(), cksum(contents.as_bytes())); + self + } + + fn build(&mut self) { + let p = self.p.take().unwrap(); + let json = json::encode(&self.cksum).unwrap(); + let p = p.file(".cargo-checksum.json", &json); + p.build(); + } +} + +#[test] +fn simple() { + setup(); + + VendorPackage::new("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "pub fn foo() {}") + .build(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", r#" + extern crate foo; + + pub fn bar() { + foo::foo(); + } + "#); + p.build(); + + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.1.0 ([..]bar) +[FINISHED] [..] +")); +} + +#[test] +fn not_there() { + setup(); + + project("index").build(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", r#" + extern crate foo; + + pub fn bar() { + foo::foo(); + } + "#); + p.build(); + + assert_that(p.cargo("build"), + execs().with_status(101).with_stderr("\ +error: no matching package named `foo` found (required by `bar`) +location searched: [..] +version required: ^0.1.0 +")); +} + +#[test] +fn multiple() { + setup(); + + VendorPackage::new("foo-0.1.0") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "pub fn foo() {}") + .file(".cargo-checksum", "") + .build(); + + VendorPackage::new("foo-0.2.0") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.2.0" + authors = [] + "#) + .file("src/lib.rs", "pub fn foo() {}") + .file(".cargo-checksum", "") + .build(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", r#" + extern crate foo; + + pub fn bar() { + foo::foo(); + } + "#); + p.build(); + + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.1.0 ([..]bar) +[FINISHED] [..] +")); +} + +#[test] +fn crates_io_then_directory() { + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", r#" + extern crate foo; + + pub fn bar() { + foo::foo(); + } + "#); + p.build(); + + let cksum = Package::new("foo", "0.1.0") + .file("src/lib.rs", "pub fn foo() -> u32 { 0 }") + .publish(); + + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `[..]` +[DOWNLOADING] foo v0.1.0 ([..]) +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.1.0 ([..]bar) +[FINISHED] [..] +")); + + setup(); + + let mut v = VendorPackage::new("foo"); + v.file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#); + v.file("src/lib.rs", "pub fn foo() -> u32 { 1 }"); + v.cksum.package = cksum; + v.build(); + + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.1.0 ([..]bar) +[FINISHED] [..] +")); +} + +#[test] +fn crates_io_then_bad_checksum() { + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", ""); + p.build(); + + Package::new("foo", "0.1.0").publish(); + + assert_that(p.cargo("build"), + execs().with_status(0)); + setup(); + + VendorPackage::new("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "") + .build(); + + assert_that(p.cargo("build"), + execs().with_status(101).with_stderr("\ +error: checksum for `foo v0.1.0` changed between lock files + +this could be indicative of a few possible errors: + + * the lock file is corrupt + * a replacement source in use (e.g. a mirror) returned a different checksum + * the source itself may be corrupt in one way or another + +unable to verify that `foo v0.1.0` was the same as before in any situation + +")); +} + +#[test] +fn bad_file_checksum() { + setup(); + + VendorPackage::new("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "") + .build(); + + let mut f = t!(File::create(paths::root().join("index/foo/src/lib.rs"))); + t!(f.write_all(b"fn foo() -> u32 { 0 }")); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", ""); + p.build(); + + assert_that(p.cargo("build"), + execs().with_status(101).with_stderr("\ +error: the listed checksum of `[..]lib.rs` has changed: +expected: [..] +actual: [..] + +directory sources are not intended to be edited, if modifications are \ +required then it is recommended that [replace] is used with a forked copy of \ +the source +")); +} diff --git a/tests/install.rs b/tests/install.rs index 52576d5bb..af6efcfad 100644 --- a/tests/install.rs +++ b/tests/install.rs @@ -26,7 +26,7 @@ fn pkg(name: &str, vers: &str) { extern crate {}; fn main() {{}} ", name)) - .publish() + .publish(); } #[test] @@ -61,8 +61,6 @@ fn pick_max_version() { assert_that(cargo_process("install").arg("foo"), execs().with_status(0).with_stderr(&format!("\ [UPDATING] registry `[..]` -[DOWNLOADING] foo v0.0.2 (registry file://[..]) -[COMPILING] foo v0.0.2 (registry file://[..]) [DOWNLOADING] foo v0.0.2 (registry [..]) [COMPILING] foo v0.0.2 [FINISHED] release [optimized] target(s) in [..] diff --git a/tests/local-registry.rs b/tests/local-registry.rs index 906e7cde2..43bf1fa29 100644 --- a/tests/local-registry.rs +++ b/tests/local-registry.rs @@ -53,9 +53,12 @@ fn simple() { [UNPACKING] foo v0.0.1 ([..]) [COMPILING] foo v0.0.1 [COMPILING] bar v0.0.1 ({dir}) +[FINISHED] [..] ", dir = p.url()))); - assert_that(p.cargo("build"), execs().with_status(0).with_stderr("")); + assert_that(p.cargo("build"), execs().with_status(0).with_stderr("\ +[FINISHED] [..] +")); assert_that(p.cargo("test"), execs().with_status(0)); } @@ -90,6 +93,7 @@ fn multiple_versions() { [UNPACKING] foo v0.1.0 ([..]) [COMPILING] foo v0.1.0 [COMPILING] bar v0.0.1 ({dir}) +[FINISHED] [..] ", dir = p.url()))); @@ -143,6 +147,7 @@ fn multiple_names() { [COMPILING] [..] [COMPILING] [..] [COMPILING] local v0.0.1 ({dir}) +[FINISHED] [..] ", dir = p.url()))); } @@ -187,6 +192,7 @@ fn interdependent() { [COMPILING] foo v0.0.1 [COMPILING] bar v0.1.0 [COMPILING] local v0.0.1 ({dir}) +[FINISHED] [..] ", dir = p.url()))); } @@ -247,6 +253,7 @@ fn path_dep_rewritten() { [COMPILING] foo v0.0.1 [COMPILING] bar v0.1.0 [COMPILING] local v0.0.1 ({dir}) +[FINISHED] [..] ", dir = p.url()))); } diff --git a/tests/publish.rs b/tests/publish.rs index 6b7b19bb0..38ccad219 100644 --- a/tests/publish.rs +++ b/tests/publish.rs @@ -355,7 +355,7 @@ fn dry_run() { assert_that(p.cargo_process("publish").arg("--dry-run") .arg("--host").arg(registry().to_string()), execs().with_status(0).with_stderr(&format!("\ -[UPDATING] registry `{reg}` +[UPDATING] registry `[..]` [WARNING] manifest has no documentation, [..] [PACKAGING] foo v0.0.1 ({dir}) [VERIFYING] foo v0.0.1 ({dir}) @@ -364,8 +364,7 @@ fn dry_run() { [UPLOADING] foo v0.0.1 ({dir}) [WARNING] aborting upload due to dry run ", - dir = p.url(), - reg = registry()))); + dir = p.url()))); // Ensure the API request wasn't actually made assert!(!upload_path().join("api/v1/crates/new").exists()); -- 2.30.2